unit grounds;

{*
Simplified OOP wrapper for the SOAR rendering engine
TODO: this code need serious cleanup, also it has too many public variables
}

interface

uses
  Classes, SysUtils, glSOARHelpers, glSOARMain, VUMaths, Cameras,
  ImagingUtility, gl, glu, glut, glext, opengls, keyboards, textures, debugs,
  vectors;

{ TGround }
type
  TGround = class
  protected
    FGrab : boolean;
    FFirstGrab : boolean;
    FFirstGrabUpdate : boolean;
    procedure SetGrab(AValue : boolean);
  public
    // initial variables
    Ground: string; // e.g. "desert", or "volcano", from /data/ground/
    Size: longint; // terrain virtual Size
    ZScale: real;
    Blocks: longint;
    StartX: longint;
    StartY: longint;
    // runtime attributes
    Filter8BitHeightMap: boolean;
    WireOverlay: boolean;
    Flying: boolean;
    Texturing: boolean;
    Fog: boolean;
    FogDensity : real;
    Detail: boolean;
    CullFovX: single;
    // camera variables
    CameraMode: TCameraMode;
    Position: TVector3;
    Direction: TVector3;
    ViewUp: TVector3;
    ViewAt: TVector3;
    ViewEye: TVector3;
    Frustum: TFrustum;
    // internal variables
    FovX: single;
    NearZ: single;
    FarZ: single;
    DetailRepeat: single;
    SkyColor: TVector4;
    GroundColor: TVector4;
    TrianglesPerFrame: longint;
    TrianglesDrawn: longint;
    TotalTerrainTriangles: longInt;
    // lights
    GroundAmbient : array[0..3] of glfloat;
    // textures
    GroundTexture: TTexture;
    DetailTexture: TTexture;
    GrabGroundRotation : real;

    constructor Create; virtual;
    destructor Destroy; override;
    procedure Initialize(APath,AGround : string); virtual;
    procedure DrawTerrainBlock(AX, AY: longint);

    procedure UpdateGrab;
    procedure UpdateCamera(APlayer, APlayerRotation : TVector3; AEyesM : TMatrix4);
    procedure Draw;
    procedure UpdateCamHeight;
    procedure UpdateFog;
  published
    property Grab : boolean read FGrab write SetGrab;
  end;

var
  Ground: TGround;

procedure GroundPassiveMotion(X, Y: LongInt); cdecl;

implementation

{ TGround }

constructor TGround.Create;
begin
  // initial variables
  Ground := '';
  Size := 1025; // must be 2^n+1
  ZScale := 0.25;
  Blocks := 1;
  StartX := 0;
  StartY := 0;
  // runtime variables
  Filter8BitHeightMap := true;
  WireOverlay := false;
  Flying := false;
  Texturing := true;
  Fog := true;
  FogDensity := 1.0;
  Detail := true;
  CullFovX := 45.0;
  // camera
  CameraMode := cmFree;
  Position := Vector3(0, 0, 0);
  Direction := Vector3(1, 0.8, -0.2);
  // internal variables
  FovX := 45.0;
  NearZ := 0.1;
  FarZ := 131072.0;
  DetailRepeat := 2.0;
  SkyColor := Vector4(0.5, 0.625, 0.75, 1.0);
  GroundColor := Vector4(0.60, 0.45, 0.30, 1.0);
  TrianglesPerFrame := 0;
  TrianglesDrawn := 0;
  TotalTerrainTriangles := 0;
  // lights
  GroundAmbient[0] := 5;
  GroundAmbient[1] := 5;
  GroundAmbient[2] := 5;
  GroundAmbient[3] := 1;
end;

destructor TGround.Destroy;
begin
  TextureList.FreeTexture(GroundTexture);
  TextureList.FreeTexture(DetailTexture);
  //FIXME: now, you can have only 1 ground because SOAR code use global variables!
  FreeTerrain;
  inherited Destroy;
end;

procedure TGround.UpdateFog;
begin
  if Fog then
    glEnable(GL_FOG)
  else
    glDisable(GL_FOG);
  glHint(GL_FOG_HINT, GL_NICEST);
  glFogi(GL_FOG_MODE, GL_EXP2);
  glFogf(GL_FOG_DENSITY, FogDensity / (Size * Sqrt(Blocks)));
  glFogfv(GL_FOG_COLOR, @SkyColor);
end;

procedure TGround.Initialize(APath, AGround : string);
begin
  Ground := AGround;
  // check the ground Size
  if not (IsPow2(Size) or IsPow2(Size - 1)) then
    raise Exception.Create('Invalid ground size ' + IntToStr(size));
  Position := Vector3(StartX / 100 * Size, StartY / 100 * Size, 0);

  // Set default GL settings
  glHint(GL_PERSPECTIVE_CORRECTION_HINT, GL_NICEST);
  glHint(GL_POINT_SMOOTH_HINT, GL_NICEST);
  glHint(GL_LINE_SMOOTH_HINT, GL_NICEST);
  glHint(GL_POLYGON_SMOOTH_HINT, GL_NICEST);

  glDisable(GL_LIGHTING);
  glDisable(GL_BLEND);
  glEnable(GL_CULL_FACE); // back face of ground doesn't need to be displayed
  glEnable(GL_DEPTH_TEST);
  glEnable(GL_TEXTURE_2D);

  glPolygonMode(GL_FRONT_AND_BACK, GL_FILL);
  glShadeModel(GL_SMOOTH);
  glDepthFunc(GL_LEQUAL);
  glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);

  glEnableClientState(GL_VERTEX_ARRAY);

  // Load terrain and textures
  MsgOut(Format('Loading terrain (size: %dx%d, blocks: %dx%d)', [Size,
    Size, Blocks, Blocks]));
  if Ground <> '' then
  begin
    // check
    if not FileExists(APath+'ground/' + Ground + '/heightmap.jpg') then
    raise Exception.Create('Soar heightmap not found - '+APath+'ground/' + AGround + '/heightmap.jpg');
    // load
    LoadTerrain(APath+'ground/' + Ground + '/heightmap.jpg', Size, ZScale, Blocks > 1, Filter8BitHeightMap);
    MsgOut('Heightmap Loaded');
    GroundTexture := TextureList.Load(APath+'ground/' + Ground + '/texture.jpg');
    MsgOut('Texture Loaded');
    DetailTexture := TextureList.Load(APath+'ground/' + Ground + '/detail.jpg');
    MsgOut('Detail Loaded');
  end;
  TotalTerrainTriangles := Size * Size * 2 * Blocks * Blocks;
end;

procedure TGround.DrawTerrainBlock(AX, AY: longint);
var
  ShiftVector: TVector3;
  EdgeFlags: TEdgeFlags;
begin
  ShiftVector := Vector3(AX * (Size - 1), AY * (Size - 1), 0.0);
  EdgeFlags := [];
  if (AX >= 0) and (AX < Blocks - 1) then
    Include(EdgeFlags, efPosX);
  if (AX > 0) and (AX <= Blocks - 1) then
    Include(EdgeFlags, efNegX);
  if (AY >= 0) and (AY < Blocks - 1) then
    Include(EdgeFlags, efPosY);
  if (AY > 0) and (AY <= Blocks - 1) then
    Include(EdgeFlags, efNegY);
  MeshRefine(ScreenWidth, FovX, EdgeFlags, ViewEye, ShiftVector, Frustum);
  // Enable/disable textures
  if Texturing then
  begin
    glEnable(GL_TEXTURE_2D);
    if MultiTexturing then
    begin
      glActiveTextureARB(GL_TEXTURE1_ARB);
      if Detail then
        glEnable(GL_TEXTURE_2D)
      else
        glDisable(GL_TEXTURE_2D);
      glActiveTextureARB(GL_TEXTURE0_ARB);
    end;
  end else
  begin
    DisableTextures;
    glColor4fv(@GroundColor);
  end;
  if WireOverlay then
  begin
    glEnable(GL_POLYGON_OFFSET_FILL);
    glPolygonOffset(1.0, 4.0);
  end;
  glMatrixMode(GL_MODELVIEW);
  glPushMatrix;
  glTranslatef(ShiftVector.X, ShiftVector.Y, ShiftVector.Z);
  glVertexPointer(3, GL_FLOAT, SizeOf(TVertex), @VertexBuffer[0].Pos);
  // Draw terrain block
  try
    glDrawElements(GL_TRIANGLE_STRIP, NumIndices, GL_UNSIGNED_INT, IndexBuffer);
  except
    Debug.Msg(dlInfo,'exception: on glDrawElements in grounds.pas');
  end;
  // Draw block edges if necessary
  if efPosX in EdgeFlags then
    glDrawElements(GL_TRIANGLES, NumPosXEdgeIndices, GL_UNSIGNED_INT, PosXEdgeIndices);
  if efPosY in EdgeFlags then
    glDrawElements(GL_TRIANGLES, NumPosYEdgeIndices, GL_UNSIGNED_INT, PosYEdgeIndices);
  if efNegX in EdgeFlags then
    glDrawElements(GL_TRIANGLES, NumNegXEdgeIndices, GL_UNSIGNED_INT, NegXEdgeIndices);
  if efNegY in EdgeFlags then
    glDrawElements(GL_TRIANGLES, NumNegYEdgeIndices, GL_UNSIGNED_INT, NegYEdgeIndices);
  if WireOverlay then
  begin
    glPushAttrib(GL_ENABLE_BIT or GL_COLOR_BUFFER_BIT or GL_POLYGON_BIT);
    glPolygonMode(GL_FRONT_AND_BACK, GL_LINE);
    glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
    glEnable(GL_BLEND);
    glEnable(GL_LINE_SMOOTH);
    DisableTextures;
    glDrawElements(GL_TRIANGLE_STRIP, NumIndices, GL_UNSIGNED_INT, IndexBuffer);
    glPopAttrib;
    glDisable(GL_POLYGON_OFFSET_FILL);
  end;
  glPopMatrix;
  Inc(TrianglesDrawn, NumTriangles);
end;

procedure TGround.Draw;
var
  x, y: integer;
  TexGenSPlane, TexGenTPlane: TVector4;
begin
  glFrontFace(GL_CW);

  // lights
  glEnable(GL_LIGHTING);
  glDisable(GL_LIGHT0);
  glEnable(GL_LIGHT1);
  glLightfv(GL_LIGHT1, GL_AMBIENT, @GroundAmbient);

  // fog
  UpdateFog;

  // Set ground texture
  if Assigned(GroundTexture) then
  begin
    glBindTexture(GL_TEXTURE_2D, GroundTexture.Id);
    glTexEnvi(GL_TEXTURE_ENV, GL_TEXTURE_ENV_MODE, GL_MODULATE);
    glEnable(GL_TEXTURE_GEN_S);
    glEnable(GL_TEXTURE_GEN_T);
    glTexGeni(GL_S, GL_TEXTURE_GEN_MODE, GL_OBJECT_LINEAR);
    glTexGeni(GL_T, GL_TEXTURE_GEN_MODE, GL_OBJECT_LINEAR);
    TexGenSPlane := Vector4(1.0 / Size, 0.0, 0.0, 0.0);
    TexGenTPlane := Vector4(0.0, 1.0 / Size, 0.0, 0.0);
    glTexGenfv(GL_S, GL_OBJECT_PLANE, @TexGenSPlane);
    glTexGenfv(GL_T, GL_OBJECT_PLANE, @TexGenTPlane);
  end;

  // Set detail texture if multitexturing is supported
  MultiTexturing := Load_GL_ARB_multitexture;
  if MultiTexturing and assigned(DetailTexture) then
  begin
    glActiveTextureARB(GL_TEXTURE1_ARB);
    glBindTexture(GL_TEXTURE_2D, DetailTexture.Id);
    glTexEnvi(GL_TEXTURE_ENV, GL_TEXTURE_ENV_MODE, GL_COMBINE_ARB);
    glTexEnvi(GL_TEXTURE_ENV, GL_RGB_SCALE_ARB, 2);
    glEnable(GL_TEXTURE_GEN_S);
    glEnable(GL_TEXTURE_GEN_T);
    glTexGeni(GL_S, GL_TEXTURE_GEN_MODE, GL_OBJECT_LINEAR);
    glTexGeni(GL_T, GL_TEXTURE_GEN_MODE, GL_OBJECT_LINEAR);
    TexGenSPlane := Vector4(1.0 / DetailRepeat, 0.0, 0.0, 0.0);
    TexGenTPlane := Vector4(0.0, 1.0 / DetailRepeat, 0.0, 0.0);
    glTexGenfv(GL_S, GL_OBJECT_PLANE, @TexGenSPlane);
    glTexGenfv(GL_T, GL_OBJECT_PLANE, @TexGenTPlane);
    glActiveTextureARB(GL_TEXTURE0_ARB);
  end;

  // Draw the ground and update camera stuff and other parts
  // Update camera vectors
  ViewEye := Vector3(Position.X, Position.Y, Position.Z + 1.60);
  ViewAt := Vector3(Position.X + Direction.X, Position.Y + Direction.Y, Position.Z + Direction.Z + 1.60);
  ViewUp := Vector3(0.0, 0.0, 1.0);

  // Set camera position and target point
  glMatrixMode(GL_MODELVIEW);
  glLoadIdentity;
  gluLookAt(ViewEye.X, ViewEye.Y, ViewEye.Z,
    ViewAt.X, ViewAt.Y, ViewAt.Z,
    ViewUp.X, ViewUp.Y, ViewUp.Z);

  // Update frustum for view culling
  glMatrixMode(GL_PROJECTION);
  glPushMatrix;
  glLoadIdentity;
  gluPerspective(CullFovX, ScreenWidth / ScreenHeight, NearZ, FarZ);
  UpdateFrustum(Frustum);
  glPopMatrix;
  glMatrixMode(GL_MODELVIEW);

  glClearColor(SkyColor.X, SkyColor.Y, SkyColor.Z, 1.0);
  glClearDepth(1.0);
  glClear(GL_COLOR_BUFFER_BIT or GL_DEPTH_BUFFER_BIT);

  // Draw individual terrain blocks
  for X := 0 to Blocks - 1 do
    for Y := 0 to Blocks - 1 do
      DrawTerrainBlock(X, Y);

  // disable texture to not display detail texture on following Models
  DisableTextures;
  glFrontFace(GL_CCW);
  glEnable(GL_LIGHT0);
  glDisable(GL_LIGHT1);
end;

procedure TGround.UpdateGrab;
{*
Keep mouse in center of screen and pan viewport accordingly, use F9 to on/off this feature
}
var
  dx, dy : integer;
  AngleXY, AngleXZ : real;
begin
  // F9 enable or disable mouse grab
  if KeyTrigger(vkF9) then
  begin
    FGrab := not FGrab;
    if FGrab then
      ReturnMouse
    else
      glutSetCursor(GLUT_CURSOR_LEFT_ARROW);
    FFirstGrab := true;
    Debug.Msg(dlInfo,'mouse: grab is '+BoolToStr(FGrab,true));
  end;
  
  // do not continue if grub is enabled, has not been set or mouse didnt moved
  if not FGrab then 
    exit;
  if FFirstGrab then
    exit;
  if FFirstGrabUpdate then
  begin
    OldMouseX := MouseX;
    OldMouseY := MouseY;
    FFirstGrabUpdate := false;
    exit;
  end;
  if (MouseX = OldMouseX)
  and(MouseY = OldMouseY) then
    exit;
  // Get angles between X/Y and X/Z planes from camera direction and update them
  dx := MouseX - OldMouseX;
  dy := MouseY - OldMouseY;
  //writeln('--- GrabUpdate: dx=',dx,' dy=',dy);
  // according to mouse movement
  AngleXY := 1;
  AngleXZ := 1;
  if FGrab then
  begin
    try
      AngleXY := ArcCos(Vec3Dot(Vec3Normalize(Vector3(Direction.X, Direction.Y, 0.0)), Vec3XUnit));
    except
      Debug.Msg(dlInfo,'except: dir:'+Vector3ToStr(Direction));
      AngleXY := 0;
    end;
    if Direction.Y < 0.0 then
      AngleXY := -AngleXY;
    AngleXZ := -ArcSin(Direction.Z);
    //writeln('mx=',mousex,' ox=',oldmousex,' my=',mousey,' oy=',oldmousey);
    //writeln('AngleXY=',AngleXY:1:8,' AngleXZ=',AngleXZ:1:8,' ');
    AngleXY := AngleXY - (dx * 0.01);
    AngleXZ := AngleXZ + (dy * 0.01);
    // near zenit and botom it act weird so avoid it
    if AngleXZ > 1 then
      AngleXZ := 1;
    if AngleXZ < -1 then
      AngleXZ := -1;
    // set the direction and return mouse to center of screen
    ReturnMouse;
    Direction := Vector3(Cos(AngleXZ) * Cos(AngleXY), Cos(AngleXZ) * Sin(AngleXY),-Sin(AngleXZ)); 
  end else
    glutSetCursor(GLUT_CURSOR_LEFT_ARROW);
  GrabGroundRotation := AngleXY;
end;

procedure GroundPassiveMotion(X, Y: LongInt); cdecl;
{*
Used for mouse grab
}
begin
  //writeln('m=',x,',',y,' o=',OldMouseX,',',OldMouseY);
  if Ground.FFirstGrab then
  begin
    OldMouseX := X;
    OldMouseY := Y;
    Ground.FFirstGrab := false;
  end;
  MouseX := X;
  MouseY := Y;
end;

procedure TGround.SetGrab(AValue : boolean);
{*
Set grab
}
begin
  FGrab := AValue;
  FFirstGrab := true;
  FFirstGrabUpdate := true;
end;

procedure TGround.UpdateCamera(APlayer, APlayerRotation : TVector3; AEyesM : TMatrix4);
{*
Free camera movement using arrow keys
}
begin
  // switch camera mode
  if KeyTrigger(vkF1) then begin CameraMode := cmFirstPerson;  Debug.Msg(dlInfo,'CameraMode: cmFirstPerson'); end;
  if KeyTrigger(vkF2) then
  begin
    CameraMode := cmSecondPerson;
    Debug.Msg(dlInfo,'CameraMode: cmSecondPerson');
  end;
  if KeyTrigger(vkF3) then begin CameraMode := cmThirdPerson;  Debug.Msg(dlInfo,'CameraMode: cmThirdPerson'); end;
  if KeyTrigger(vkF4) then begin CameraMode := cmFree;         Debug.Msg(dlInfo,'CameraMode: cmFree'); end;

  // first person camera stick postion with player's head
  if CameraMode = cmFirstPerson then
  begin
    Position := APlayer;
    // FIXME: this does not work for crouch because right now AEyesM is calculated from first frame which is always the same
    Position.Z := Position.Z + AEyesM.Singles[14]*0.3;
  end;

  // second person - free position but view to player
  if CameraMode = cmSecondPerson then
  begin
    // if i am too close to player, move little back away
    if Vec3Length(Vec3Subtract(APlayer,Position)) < 50 then
    begin
      Position.X := Position.X - 1;
    end;
    UpdateCamHeight;
    Direction := Vec3Normalize(Vec3Add(APlayer,Vec3Negate(Position)));
    //writeln('2nd cam: L = ',Vec3Length(Vec3Subtract(APlayer,Position)):1:8);
  end;

  // third person
  if CameraMode = cmThirdPerson then
  begin
    // position above player's head
    Position := APlayer;
    Position.Z := Position.Z + AEyesM.Singles[14]*0.3;
    // move little behind him
    Position.X := Position.X + 50 * sin(pi*(270-APlayerRotation.Z)/180);
    Position.Y := Position.Y + 50 * cos(pi*(270-APlayerRotation.Z)/180);
    UpdateCamHeight;
    Direction := Vec3Normalize(Vec3Add(Vector3(APlayer.X,APlayer.Y,APlayer.Z+AEyesM.Singles[14]*0.3/2),Vec3Negate(Position)));
  end;

  // free camera mode alows free movement using arrows
  if (CameraMode = cmFree)or(CameraMode = cmSecondPerson) then
  begin
    if KeyIsDown(vkUp) then    Position := Vec3Add(Position,Direction);
    if KeyIsDown(vkDown) then  Position := Vec3Add(Position,Vec3Negate(Direction));
    if KeyIsDown(vkRight) then Position := Vec3Add(Position,Vec3Cross(Direction,ViewUp));
    if KeyIsDown(vkLeft) then  Position := Vec3Add(Position,Vec3Negate(Vec3Cross(Direction,ViewUp)));
    if KeyIsDown(vkPgUp) then  Position := Vec3Add(Position,ViewUp);
    if KeyIsDown(vkPgDn) then  Position := Vec3Add(Position,Vec3Negate(ViewUp));
  end;
end;

procedure TGround.UpdateCamHeight;
{*
Never allow place cam under the ground
}
var z : real;
begin
  z := GetTerrainZ(Position.X, Position.Y, Position.Z);
  if Position.Z < z + 15 then
    Position.Z := z + 15;
end;

{procedure TGround.CalculateGroundProjection(ADirection : TVector3);
var AngleXY, AngleXZ, AngleYZ : real;
begin
  try
    AngleXY := ArcCos(Vec3Dot(Vec3Normalize(Vector3(ADirection.X, ADirection.Y, 0.0)), Vec3XUnit));
  except
    Debug.Msg(dlInfo,'TGround.CalculateGroundProjection: dir:'+Vector3ToStr(Direction));
    AngleXY := 0;
  end;
  if ADirection.Y < 0.0 then
    AngleXY := -AngleXY;
  AngleXZ := -ArcSin(ADirection.Z);
  AngleXY := AngleXY - (dx * 0.01);
  AngleXZ := AngleXZ + (dy * 0.01);
  // near zenit and botom it act weird so avoid it
  if AngleXZ > 1 then
    AngleXZ := 1;
  if AngleXZ < -1 then
    AngleXZ := -1;
  // set the direction and return mouse to center of screen
  Direction := Vector3(Cos(AngleXZ) * Cos(AngleXY), Cos(AngleXZ) * Sin(AngleXY),-Sin(AngleXZ)); 
end;}

initialization

  Ground := TGround.Create;

finalization

  Debug.Msg(dlInfo,'Releasing Ground');
  Ground.Free;

end.
